大家好,我是 Yubin
在網頁的應用上,認證 (Authentication) 與授權 (Authorization) 是非常重要的。
本篇文章會介紹兩者的差異以及使用 @fastify/basic-auth
來實作 HTTP 基本認證 (Basic Auth)。
因為都是 Auth 開頭所以常常有人會搞混,簡單說就是:
Authentication,認證,你是誰。
Authorization,授權,你有沒有權利進行那項操作。
網頁應用上,伺服器常常需要知道發送請求的人是誰,用戶端發送 Request 要帶上足夠的資訊證明自己的身份,伺服器收到後,藉著那些資訊去判斷這個 Request 是否真的是那個人,這是認證 (Authentication) 的動作。
如果該 Request 沒有帶上足夠的資訊或認證失敗,伺服器應該回應 401 Unauthorized
的狀態碼。
認證動作完成,拿到 Request 發送方的身份後,因為有些動作不是每位使用者都有權限操作,判斷該身份是否有權限進行某個動作,這個是授權 (Authorization)。常見的作法是利用角色 (Role) 來劃分權限。
如果該 Request 代表的身份沒有權限進行當前的操作,伺服器應該回應 403 Forbidden
。
現實上,很多伺服器的實作不論認證或授權,只要失敗都會回應
403 Forbidden
,但我們在開發後端應用程式的時候,提供足夠精確的狀態碼,更能讓存取你的資源的人明白發生了什麼事。
認證的方式有許多種,其中 HTTP 基本認證 (Basic Access Authentication) 是一個常見且簡單的認證方式。
Basic Auth 利用把使用者帳號、密碼帶在 Request 的 HTTP Header 上,藉由這樣的方式把資訊送給伺服器做認證,伺服器收到資訊後,就可以去檢查帳號密碼有沒有匹配,以此來得知發送這個 Request 的使用者是誰。
帳號密碼的資訊會放在 Header 的 Authorization
欄位。
假設有一個使用者帳號是 user01
,密碼是 user01password
。
會先用冒號 (:
) 串起來:
user01:user01password
接著將串起來的字串進行 Basic64 編碼:
dXNlcjAxOnVzZXIwMXBhc3N3b3Jk
編碼完,開頭串上 Basic
(注意要用空格做分隔),然後放進 HTTP 的 Authorization
中:
Authorization: Basic dXNlcjAxOnVzZXIwMXBhc3N3b3Jk
發送給伺服器的 Request 就是利用這個方式把 Basic Auth 的資訊帶上。
伺服器端也有要有相應的實作,才能處理用戶端帶過來的 Basic Auth 資訊。
因為 Basic Auth 非常單純,所以我們只要把 Request 上面的 Authorization
欄位拿出來,做 Base64 解碼,再用冒號 (:
) 進行字串切割,就可以拿到使用者的帳號密碼了。
但這個步驟,雖然簡單但也有點麻煩,
我們可以使用 Fastify 官方維護的 Plugin: @fastify/basic-auth 來幫助我們。
利用 npm 進行安裝:
npm i @fastify/basic-auth
安裝好後透過 server.register()
進行註冊,然後帶入需要使用的 Validatation Handler。
import fastify, {
FastifyInstance,
FastifyReply,
FastifyRequest,
DoneFuncWithErrOrRes
} from 'fastify'
import fastifyBasicAuth from '@fastify/basic-auth'
function validate(
username: string,
password: string,
request: FastifyRequest,
reply: FastifyReply,
done: DoneFuncWithErrOrRes
) {
// implement your validate mechanism
if (username === 'user01' && password === 'user01password') {
done()
} else {
done(new Error('username or password incorrect'))
}
}
server.register(fastifyBasicAuth, { validate })
註冊 plugin 的時候要帶入 Validatation Handler,該函式可以拿到解碼出來的 username
, password
。
可以在這裡實作認證機制。
可以參考官方文件來看更多可用選項。
以上程式完成了 @fastify/basic-auth 的註冊及認證方法定義,但還沒有定義在哪個時機點或範圍進行認證。
如果要在每個 Request 進來的時候都進行認證,可以在 FastifyInstance 註冊 onRequest
的 hook:
server.after(() => {
server.addHook('onRequest', server.basicAuth)
})
這邊把
addHook()
放在server.after()
的 callback 中,是為了確保執行addHook()
動作的時候,所有 Plugin 都已經被註冊完成。
這樣一來,我們的程式就具備了 Basic Auth 的能力,而且對於進來的每一個 Request 都會進行認證的動作。
使用 Postman 來打打看:
因為沒有帶上 Basic Auth 的 Header,所以 Fastify App 回應 401 Unauthorized
。
利用 Postman 加上 Basic Auth 的資訊。 (畫面上只需要輸入 username 跟 password,因為 Postman 會幫我們進行編碼及套上正確的格式)
輸入正確的資訊,通過 Validatation Handler 的驗證,就可以存取到相應的資源。
如果帶入的 Basic Auth 資訊錯誤:
除了回傳 401
表示驗證不通過外,可以看到 message
欄位是我們從 Validatation Handler 那邊拋出的錯誤訊息,可以讓發送端收到未通過驗證的理由。
如果不想要每個 Request 都進行認證的工作,可以像這樣定義在 route scope 上:
server.after(() => {
server.get('/', { onRequest: server.basicAuth }, (request, reply) => {
request
return reply.status(200).send({ message: 'Hello user01' })
})
})
server.get('/hello', (request, reply) => {
return reply.status(200).send({ message: 'Hello World' })
})
上述範例程式,只有 GET /
會經過 Basic Auth 的認証,GET /hello
不需經過認證。
本篇介紹了基本的認證與授權,並以 @fastify/basic-auth
這個官方 Plugin 實作了 Fastify App 的 Basic Auth 認證機制。
但使用 Basic Auth 來進行驗證有滿多事情要注意的,因為帳號密碼只有透過 Base64 編碼,因為是編碼不是加密,所以如果沒有搭配 https,等於是把帳號密碼公開在網路傳輸上。
就算有使用 https 來做傳輸上的加密,帳號密碼的管理也要謹慎。
Basic Auth 比較適合在足夠單純的網路環境或使用情境中,要做驗證或授權,更靠譜且更多人使用的會是 Token 或 OAuth2 等技術來進行。
以上完整範例程式,可以參考 GitHub。